package mn.asri.teaproc.client;

import com.google.gwt.user.client.rpc.IsSerializable;
import java.util.HashMap;
import java.util.Map;

/**
 * @author <a href="mailto:mike.liu@aptechmongolia.edu.mn">Mike Liu</a>
 * @version $Id: BaseModel.java 5 2008-09-01 12:08:42Z mikeliucc $
 */
public abstract class BaseModel implements IsSerializable {

	protected static final char FIELD_DELIM = (char) 0x08;
	protected static final char PAIR_DELIM = (char) 0x09;
	protected static final char ARRAY_DELIM = (char) 0x0B;
	private static final String FIELD_DELIM_STR =
		new String(new char[]{FIELD_DELIM});
	private static final String PAIR_DELIM_STR =
		new String(new char[]{PAIR_DELIM});
	private static final String ARRAY_DELIM_STR =
		new String(new char[]{ARRAY_DELIM});

	protected static final char BM_DELIM = (char) 0x0C;
	protected static final char BM_FIELD_DELIM = (char) 0x0E;
	protected static final char BM_PAIR_DELIM = (char) 0x0F;
	protected static final char BM_ARRAY_DELIM = (char) 0x1E;
	private static final String BM_PAIR_DELIM_STR =
		new String(new char[]{BM_PAIR_DELIM});
	private static final String BM_FIELD_DELIM_STR =
		new String(new char[]{BM_FIELD_DELIM});
	private static final String BM_ARRAY_DELIM_STR =
		new String(new char[]{BM_ARRAY_DELIM});

	protected static final int INT_NULL = Integer.MIN_VALUE;
	protected static final int INT_CONVERSION_ERROR = Integer.MIN_VALUE + 2;
	protected static final long LONG_NULL = Long.MIN_VALUE;
	protected static final long LONG_CONVERSION_ERROR = Long.MIN_VALUE + 2;
	protected static final boolean BOOL_NULL = false;

	// map of string-string
	protected Map data;

	public BaseModel() { data = new HashMap(); }

	public String deserialize() { return stringify(); }

	public void serialize(String decrypted) { objectify(decrypted); }

	protected void objectify(String stringified) {
		// split on the FIELD_DELIM (field delimiter)
		String[] pairs = stringified.split(FIELD_DELIM_STR);

		// first pair not used here.  Originally I intended to use it to
		// double-check class compatibility.  However, getClass() is not "yet"
		// supported in GWT 1.4
		//String notUsed = pair;
		//if (!notUsed.equals(this.getClass().getName())) {
		//	throw new RuntimeException(
		//		"wrong class name specified from the stringified data stream!");
		//} else {
		for (int i = 1; i < pairs.length; i++) {
			String pair = pairs[i];
			int pos = pair.indexOf(PAIR_DELIM);
			String key = pair.substring(0, pos);
			String value = pair.substring(pos + 1);
			data.put(key, value);
		}
		//}

		// sub-class gets the chance to do extra steps to handle non-simple
		// data types like arrays or UDT.
		postObjectify();
	}

	/**
	 * called within the objectify() method, after the payload has been
	 * "deserialized" into the internal map storage" so that sub-class can handle
	 * the more complex data types.  The <code>objectify()</code> method will set
	 * all data from the payload to the internal map.  It is the responsibility of
	 * each subclass to handle the data individually, if need be.
	 */
	protected abstract void postObjectify();

	/** get data from the internal map storage. */
	protected String getData(String field) { return (String) data.get(field); }

	/** put data into the internal map storage. */
	protected void setData(String field, String value) {
		data.put(field, value);
	}

	/** put data into the internal map storage. */
	protected void setData(String field, BaseModel value) {
		String str = value.stringify();

		// increment level
		String levelStr = str.substring(0, 2);
		int level = Integer.parseInt(levelStr);
		int newLevel = level + 1;
		String newLevelStr = (newLevel < 10) ? "0" + newLevel : newLevel + "";
		str = newLevelStr + str.substring(2);

		// replace delimeter
		str = str.replaceAll(FIELD_DELIM_STR, BM_FIELD_DELIM_STR);
		str = str.replaceAll(PAIR_DELIM_STR, BM_PAIR_DELIM_STR);
		str = str.replaceAll(ARRAY_DELIM_STR, BM_ARRAY_DELIM_STR);

		data.put(field, BM_DELIM + str + BM_DELIM);
	}

	/**
	 * "serialize" data from the internal map storage to a string.  In this method,
	 * the <code>preStringify()</code> is called so that subclasses can further
	 * manipulate the data in the internal map storage.  After that, this method
	 * "converts" all data within the map into a string-only name-value pair.
	 */
	protected String stringify() {
		preStringify();
		StringBuffer buffer = new StringBuffer("01");
		buffer.append(FIELD_DELIM);

		Object[] keys = data.keySet().toArray();
		for (int i = 0; i < keys.length; i++) {
			String key = (String) keys[i];
			String value = (String) data.get(key);
			buffer.append(key).append(PAIR_DELIM).append(value).
				append(FIELD_DELIM);
		}

		return buffer.toString();
	}

	/**
	 * called within the <code>stringify()</code> before the actual "serialization"
	 * starts.  This gives subclasses a chance to prepare the data in the internal
	 * map storage, if need be.
	 */
	protected abstract void preStringify();

	/**
	 * convert the string version of <code>data</code> into its numeric form. Since
	 * this class will be used on the client-side, logging and exception handling
	 * is tricky.  Thus a special <code>INT_CONVERSION_ERROR</code> constants
	 * denotes the presence of a string-to-int conversion error.
	 *
	 * @see #INT_CONVERSION_ERROR
	 */
	protected static int toInt(String data) {
		if (data == null || data.length() < 1) { return INT_NULL; }
		// we need better checking here
		try {
			return Integer.parseInt(data);
		} catch (NumberFormatException e) {
			return INT_CONVERSION_ERROR;
		}
	}

	/**
	 * convert the string version of <code>data</code> into its numeric form. Since
	 * this class will be used on the client-side, logging and exception handling
	 * is tricky.  Thus a special <code>LONG_CONVERSION_ERROR</code> constants
	 * denotes the presence of a string-to-long conversion error.
	 *
	 * @see #LONG_CONVERSION_ERROR
	 */
	protected static long toLong(String data) {
		if (data == null || data.length() < 1) { return LONG_NULL; }
		// we need better checking here
		return Long.parseLong(data);
	}

	/**
	 * convert the string version of <code>data</code> into a BaseModel form. Since
	 * this class will be used on the client-side, logging and exception handling
	 * is tricky.
	 */
	protected static BaseModel toBaseModel(String data, BaseModel model) {
		if (data == null || data.length() < 1) { return null; }
		// we need better checking here
		if (data.charAt(0) == BM_DELIM &&
		    data.charAt(data.length() - 1) == BM_DELIM) {
			data = data.substring(1, data.length() - 2);
			data = data.replaceAll(BM_FIELD_DELIM_STR, FIELD_DELIM_STR);
			data = data.replaceAll(BM_PAIR_DELIM_STR, PAIR_DELIM_STR);
			data = data.replaceAll(BM_ARRAY_DELIM_STR, ARRAY_DELIM_STR);
		}

		model.objectify(data);
		return model;
	}
}
